package icu.funkye.confine.aop;

import java.lang.reflect.Method;
import java.util.Arrays;
import icu.funkye.confine.ConfineFilter;
import icu.funkye.confine.annotations.ConfineLimiter;
import icu.funkye.confine.annotations.DefaultFailBack;
import icu.funkye.confine.exception.ConfineException;
import icu.funkye.confine.limiter.RateLimiterFactory;
import icu.funkye.confine.properties.ConfineProperties;
import icu.funkye.confine.constants.ConfineConstant;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.CodeSignature;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

/**
 * @author jianbin.chen
 */
@ConditionalOnProperty(prefix = ConfineProperties.CONFINE_PREFIX, name = "enabled", havingValue = "true",
    matchIfMissing = true)
@Aspect
@Component
public class ConfineMethodAspect implements ApplicationContextAware {

    private static final Logger LOGGER = LoggerFactory.getLogger(ConfineMethodAspect.class);

    private static volatile ApplicationContext applicationContext;

    @Pointcut("@annotation(icu.funkye.confine.annotations.ConfineLimiter)")
    public void confineAnnotation() {}

    @Around("confineAnnotation()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        MethodSignature signature = (MethodSignature)joinPoint.getSignature();
        ConfineLimiter annotation = signature.getMethod().getAnnotation(ConfineLimiter.class);
        Long stopwatch = null;
        Long maxRt = annotation.rt();
        if (maxRt > 0) {
            stopwatch = System.currentTimeMillis();
        }
        String key = annotation.method();
        CodeSignature codeSignature = (CodeSignature)joinPoint.getSignature();
        String methodName = signature.getName();
        String[] paramNames = codeSignature.getParameterNames();
        if (StringUtils.isEmpty(key)) {
            StringBuilder sb =
                new StringBuilder(joinPoint.getTarget().getClass().getName()).append(ConfineConstant.SPLIT).append(methodName);
            for (String paramName : paramNames) {
                sb.append(ConfineConstant.SPLIT).append(paramName);
            }
            key = sb.toString();
        }
        ConfineException exception = null;
        try {
            ConfineFilter.entry(key);
            // business
            return joinPoint.proceed();
        } catch (Exception e) {
            if (e instanceof ConfineException) {
                exception = (ConfineException)e;
                if (LOGGER.isDebugEnabled()) {
                    LOGGER.debug(e.getMessage());
                }
                Class clz = annotation.failBack();
                if (clz != DefaultFailBack.class) {
                    try {
                        Object object = getBean(clz);
                        Method method = object.getClass().getMethod(methodName, codeSignature.getParameterTypes());
                        return method.invoke(object, joinPoint.getArgs());
                    } catch (NoSuchMethodException ex) {
                        e = new RuntimeException("the method could not be found: " + methodName + ", parameterNames: "
                            + Arrays.toString(paramNames));
                    } catch (BeansException bex) {
                        e = bex;
                    }
                }
            }
            throw e;
        } finally {
            if (stopwatch != null) {
                Long rt = System.currentTimeMillis() - stopwatch;
                if (LOGGER.isDebugEnabled()) {
                    LOGGER.debug("the current method: {} , rt: {}", key, rt);
                }
                if (rt > 0 && (exception == null || exception.getCode() != ConfineException.ConfineExceptionCode.RT)) {
                    RateLimiterFactory.get(key).addRt(rt);
                }
            }
        }
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        ConfineMethodAspect.applicationContext = applicationContext;
    }

    // 通过name获取 Bean.
    public static Object getBean(String name) throws BeansException {
        return applicationContext.getBean(name);
    }

    // 通过class获取Bean.
    public static <T> T getBean(Class<T> clazz) throws BeansException {
        return applicationContext.getBean(clazz);
    }

    // 通过name,以及Clazz返回指定的Bean
    public static <T> T getBean(String name, Class<T> clazz) throws BeansException {
        return applicationContext.getBean(name, clazz);
    }
}
